"""
Copyright 2009 Texas A&M University
 
Licensed under the Apache License, Version 2.0 (the "License"); you may not use 
this file except in compliance with the License. You may obtain a copy of the 
License at

http://www.apache.org/licenses/LICENSE-2.0
  
Unless required by applicable law or agreed to in writing, software distributed 
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
CONDITIONS OF ANY KIND, either express or implied. See the License for the 
specific language governing permissions and limitations under the License. 
"""

import zope.interface

from logging import getLogger

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker, eagerload
from sqlalchemy.sql.expression import and_,or_,between
from sqlalchemy.exceptions import OperationalError

from meercat.interfaces import *
from meercat.storage.interfaces import IStorage
from meercat.storage.sqlalchemy.declarative import createClasses



try:
    from multiprocessing import current_process
    local_name = lambda :'%s (%s) ' % (current_process().name, str(current_process().pid))
except ImportError:
    local_name = lambda :''

log = getLogger('meercat.storage.sqlalchemy.storage')

class SqlAlchemyStorage(object):
    zope.interface.implements(IStorage)
    
    name = None
    
    def __init__(self, name, *args, **kwargs):
        log.debug('SqlAlchemyStorage.__init__(name=%s, %s, %s)' % (name, str(args), str(kwargs)))
        self.name = name
        kwargs['strategy'] = 'threadlocal'
        self._engine_args = (args, kwargs)
        self._setupConnection()
        
    def __getstate__(self):
        log.debug('%s%s.__getstate__()' % (local_name(),str(self),))
        state = {}
        state['name'] = self.name
        state['engine'] = self._engine_args
        return state
    
    def __setstate__(self, state):
        log.debug('%s%s.__setstate__(%s)' % (local_name(),str(self),str(state)))
        self.name = state['name']
        self._engine_args = state['engine']
        self._setupConnection()
    
    def __del__(self):
        log.info('%s%s.__del__' % (local_name(),str(self),))
        return super(SqlAlchemyStorage,self).__del__()
    
    def _setupConnection(self):
        log.debug('%s._setupConnection()' % (str(self),))
        args, kwargs = self._engine_args
        self.engine = create_engine(*args, **kwargs)
        self.Base = declarative_base(name='SqlAlchemyStorageBase',
                                     bind=self.engine)
        self.Session = scoped_session(sessionmaker(bind=self.engine,
                                                   autoflush=True,
                                                   autocommit=False))
        
        for name, _class in createClasses(self).iteritems():
            setattr(self,name,_class)
    
    def adaptResource(self, resource):
        log.debug('%s.adaptResource(%s)' % (str(self),str(resource)))
        if isinstance(resource, basestring):
            id = resource
            resource=[]
        elif isinstance(resource,self.Resource):
            return resource
        elif IResource.providedBy(resource):
            id = resource.getIdentifier()
        else:
            raise Exception(u'resource must implement the IResource interface')
        
        temp = self.Session().query(self.Resource).options(eagerload('instances','_metadata')).get(id)
    
        if temp is None:
            return self.Resource(identifier=resource.getIdentifier(),
                                 instances=resource)
        
        map(temp.append, resource)
        
        return temp
    
    def adaptInstance(self, instance, resource=None):
        """Factory for creating local Instance objects """
        log.debug('%s.adaptInstance(%s)' % (str(self),str(instance)))
        if isinstance(instance, tuple):
            id = instance
        elif isinstance(instance, self.Instance):
            return instance
        elif IInstance.providedBy(instance):
            id = (instance.getIdentifier(),instance.getSource())
        else:
            raise Exception(u'instance must implement the IInstance interface')
        
        session = self.Session()
        
        temp = self.Session().query(self.Instance).options(eagerload('_metadata')).get(id)
        
        if temp is None:
            instance = self.Instance(identifier=instance.getIdentifier(),
                                     source=instance.getSource(),
                                     active=instance.isActive(),
                                     metadata=instance.getMetadata(),
                                     modified=instance.getLastModified())
            if resource is None:
                resource = self.Resource()
            
            resource.instances.append(instance)
            session.add(resource)
            
            return instance
        
        if instance.isActive():
            temp.activate()
        else:
            temp.deactivate()
        
        temp.modified = instance.getLastModified()
        
        """ Remove all metadata that is from the instance source but isn't in
        the set of metadata from the incoming instance and append only the
        necessary metadata to reduce the number of write to the database. """
        current = frozenset(filter(lambda x: x.getSource() == instance.getSource(),
                                   temp.getMetadata()))
        
        incoming = frozenset(instance.getMetadata())
        
        remove = current.difference(incoming)
        append = incoming.difference(current)
        
        for m in remove:
            temp.remove(m)
        
        for a in append:
            a.setInstance(temp)
        
        temp.append(append)
        
        return temp
    
    def adaptMetadata(self, metadata, instance=None):
        log.debug('%s.adaptMetadata(%s)' % (str(self),str(metadata)))
        if not IMetadata.providedBy(metadata):
            raise Exception(u'metadata must implement the IMetadata interface')
        
        if isinstance(metadata,self.Metadata):
            return metadata
        
        if instance is None:
            instance = metadata.getInstance()

        return self.Metadata(source=metadata.getSource(),
                             format=metadata.getFormat(),
                             value=metadata.getValue(),
                             instance=instance)
    
    def getName(self):
        """Return a name for the Storage"""
        return self.name
    
    def _action(self,instance):
        instance = self.adaptInstance(instance)
        instance.save()
        return instance
    
    def action(self, instance):
        log.debug('%s.action(%s)' % (str(self),str(instance)))
        try:
            if IInstance.providedBy(instance):
                finished = self._action(instance)
                return finished
                
            if not hasattr(instance,'__iter__'):
                raise DoesNotImplement('instance must implement the IInstance '
                                       'interface or be a container of objects that '
                                       'implement IInterface')
            
            return map(self._action, instance)
        except Exception, e:
            log.error('Exception encountered while performing action on %s, rolling back transaction: %s' % (str(instance),str(e)))
            import sys, traceback
            for line in traceback.format_exception(*sys.exc_info()):
                log.debug(line)
            self.Session.rollback()
        finally:
            self.Session.commit()
    
    def merge(self, destination, remove):
        """This method merges the remove object into the destination object.
        This operation is used when de-duping records. All of the instances from
        the remove resource should be appended to the destination object and then
        the remove object should be marked as inactive.
        
        Keyword Arguments:
        destination -- This must be an object that provides the 
          meercat.interfaces.IResource interface.
        remove -- This is either an object providing the 
          meercat.interfaces.IResource interface or an iterable collection of 
          objects that provide the meercat.interfaces.IResource interface.
        
        If the destination argument is not a meercat.interfaces.IResource, a 
        zope.interface.exceptions.DoesNotImplement exception should be raised.
        
        If the remove argument is not a meercat.interfaces.IResource or a 
        collection of them, a zope.interface.exceptions.DoesNotImplement 
        exception should be raised.
        """
        log.debug('%s.merge(%s, %s)' % (str(self),str(destination),str(remove)))
        destination = self.adaptResource(destination)
        
        if IResource.providedBy(remove) or isinstance(remove,basestring):
            remove = [remove]
        
        if not hasattr(remove,'__iter__'):
            raise DoesNotImplement('Invalid remove object')
        
        for r in remove:
            if not IResource.providedBy(r):
                r = self.adaptResource(r)
            
            for instance in r:
                if IMutableResource.providedBy(r):
                    r.remove(instance)
                destination.append(instance)
                instance.save()
            
            if IMutableResource.providedBy(r):
                r.save()
        
        destination.save()
        self.Session.commit()
    
    def getResources(self, **kwargs):
        """Search through the storage and return an iterable collection of
        objects providing the meercat.interfaces.IResource interface.
        
        Keyword arguments:
        limit -- If provided, limit the resulting collection to this size
        offset -- If provided, only provide results after this offset into the 
          result set
        from_date -- If provided, limit results to those added/updated after 
          this datetime.datetime object
        until_date -- If provided, limit results to those added/update before
          this datetime.datetime object
        source -- If provided, limit the results to those that have instances
          coming from this source
        identifier -- If provided, only return the resource that matches this
          identifer
        inactive -- If false, limit results to 'active' resources
        """
        log.debug('%s.getResources(%s)' % (str(self),str(kwargs)))
        Resource = self.Resource
        Instance = self.Instance
        
        try:
            q = self.Session().query(Resource)
            
            limit      = kwargs.get('limit',None)
            offset     = kwargs.get('offset',None)
            from_date  = kwargs.get('from_date',None)
            until_date = kwargs.get('until_date',None)
            source     = kwargs.get('source',None)
            identifier = kwargs.get('identifier',None)
            inactive   = bool(kwargs.get('inactive',False))
            
            if identifier:
                q = q.filter(Resource.id == identifier)
            
            if from_date and until_date:
                q = q.filter(Resource.instances.any(between(Instance.modified,
                                                            from_date,
                                                            until_date)))
            elif from_date:
                q = q.filter(Resource.instances.any(Instance.modified >= from_date))
            elif until_date:
                q = q.filter(Resource.instances.any(Instance.modified <= until_date))
            else:
                q = q.options(eagerload('instances','_metadata'))
            
            if source:
                q = q.filter(Resource.instances.any(Instance.source == source))
            
            if not inactive:
                q = q.filter(Resource.instances.any(Instance.active == True))
            
            if offset:
                q = q.offset(offset)
            
            if limit:
                q = q.limit(limit)
            
            log.debug('%s.getResources query: %s' % (self.getName(), q.statement))
            
            return map(IPickleable,q.all())
        finally:
            self.Session.commit()
            self.Session.close()
        
        
  
    def getInstances(self, **kwargs):
        """Search through the storage and return an iterable collection of
        objects providing the meercat.interfaces.IInstance interface.
        
        Keyword arguments:
        limit -- If provided, limit the resulting collection to this size
        offset -- If provided, only provide results after this offset into the 
          result set
        from_date -- If provided, limit results to those added/updated after 
          this datetime.datetime object
        until_date -- If provided, limit results to those added/update before
          this datetime.datetime object
        source -- If provided, limit the results to those coming from this 
          source
        identifier -- If provided, only return the resource that matches this
          identifer
        inactive -- If false, limit results to 'active' resources
        """
        log.debug('%s.getInstances(%s)' % (str(self),str(kwargs),))
        Instance = self.Instance
        
        try:
            q = self.Session().query(Instance)
            
            limit      = kwargs.get('limit',None)
            offset     = kwargs.get('offset',None)
            from_date  = kwargs.get('from_date',None)
            until_date = kwargs.get('until_date',None)
            source     = kwargs.get('source',None)
            identifier = kwargs.get('identifier',None)
            inactive   = bool(kwargs.get('inactive',False))
            
            if identifier:
                q = q.filter(Instance.id == identifier)
            
            if source:
                q = q.filter(Instance.source == source)
            
            if from_date and until_date:
                q = q.filter(between(Instance.modified,from_date,until_date))
            elif from_date:
                q = q.filter(Instance.modified >= from_date)
            elif until_date:
                q = q.filter(Instance.modified <= until_date)
            else:
                q = q.options(eagerload('_metadata'))
            
            if not inactive:
                q = q.filter(Instance.active == True)
            
            if offset:
                q = q.offset(offset)
            
            if limit:
                q = q.limit(limit)
            
            log.debug('%s.getResources query: %s' % (self.getName(), q.statement))
            
            return q.all()
        finally:
            self.Session.commit()
    
    def getResourceCount(self):
        """Return the number of resources in the storage """
        log.debug('%s.getResourceCount()' % (str(self),))
        try:
            return self.Session().query(self.Resource).count()
        finally:
            self.Session.commit()
            self.Session.close()
    
    def getInstanceCount(self):
        """Return the number of instances in the storage """
        log.debug('%s.getInstanceCount()' % (str(self),))
        try:
            return self.Session().query(self.Instance).count()
        finally:
            self.Session.commit()
            self.Session.close()
